跳到主要内容

分布式自增 ID 算法 Snowflake(雪花算法)

参考资料 Twitter雪花算法SnowFlake介绍 参考资料 Twitter 的分布式自增 ID算法 snowflake (Java版)

概述

分布式系统中,有一些需要使用全局唯一 ID的场景,这种时候为了防止 ID冲突可以使用 36位的 UUID,但是 UUID有一些缺点,首先他相对比较长,另外 UUID一般是无序的。

有些时候我们希望能使用一种简单一些的 ID,并且希望 ID能够按照时间有序生成。

而 Twitter 的 snowflake 解决了这种需求,最初 Twitter 把存储系统从 MySQL 迁移到 Cassandra,因为 Cassandra没有顺序 ID生成机制,所以开发了这样一套全局唯一 ID生成服务。

它可以每秒生成 26 万个不重复的 ID

如何存储自增 ID

0 - 41位时间戳 - 5位数据中心标识 - 5位机器标识 - 12位序列号

说明: 1位不用。二进制中最高位为 1的都是负数,但是我们生成的 id一般都使用整数,所以这个最高位固定是 0

41 位用来记录时间戳(毫秒)。可以表示 2^41−1 个数字,如果只用来表示正整数(计算机中正数包含0),可以表示的数值范围是:0 至 2^41−1,减 1 是因为可表示的数值范围是从 0 开始算的,而不是1。也就是说 41 位可以表示 2^41−1 个毫秒的值,转化成单位年则是 (2^41−1)/(1000 ∗ 60 ∗ 60 ∗ 24 ∗ 365) = 69

10位用来记录工作机器 id 可以部署在 2^10=1024 个节点,包括 5位 datacenterId 和 5位 workerId,5位(bit)可以表示的最大正整数是 2^5−1=31,即可以用 0、1、2、3、....31 这 32个数字,来表示不同的 datecenterIdworkerId

12位序列号,用来记录同毫秒内产生的不同 id。12位(bit)可以表示的最大正整数是 2^12−1=4095,即可以用 0、1、2、3、....4094 这 4095个数字,来表示同一机器同一时间截(毫秒)内产生的 4095个 ID序号

由于在 Java中 64bit的整数是 long类型,所以在 Java中 SnowFlake算法生成的 id就是 long来存储的。

雪花算法的作用

SnowFlake 可以保证:

  • 所有生成的 ID 按时间趋势递增
  • 整个分布式系统内不会产生重复 ID(因为有 datacenterIdworkerId 来做区分)

算法实现(Java)

运行时先取得时间戳

long time = System.currentTimeMillis();
System.out.println(time);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(simpleDateFormat.format(new Date(time)));

原理看注释:

/**
* <p>描述:分布式自增长ID</p>
* <pre>
* Twitter 的 Snowflake JAVA 实现方案
* </pre>
*
* 核心代码为其 IdWorker 这个类实现,其原理结构如下,分别用一个 0 表示一位,用 — 分割开部分的作用:
* 1||0---0000000000 0000000000 0000000000 0000000000 0 --- 00000 ---00000 ---000000000000
* 在上面的字符串中,第一位为未使用(实际上也可作为 long 的符号位),接下来的第 41位为毫秒级时间,
* 然后 5位 datacenter 标识位,5位机器ID(并不算标识符,实际是为线程标识),
* 然后 12位该毫秒内的当前毫秒内的计数,加起来刚好64位,为一个 Long型。
* 这样的好处是,整体上按照时间自增排序,并且整个分布式系统内不会产生 ID碰撞(由 datacenter和机器 ID作区分),
* 并且效率较高,经测试,snowflake 每秒能够产生 26万ID左右,完全满足需要。
*
*
* 64位ID (42(毫秒)+5(机器ID)+5(业务编码)+12(重复累加))
*/
public class IdWorker {
// 时间起始标记点,作为基准,一般取系统的最近时间(一旦确定不能变动)
private static final long TWEPOCH = 1618895262537L; // 2021-04-20 13:07:42

//长度为5位
private static final long WORKER_ID_BITS = 5L; // 机器标识位数
private static final long DATACENTER_ID_BITS = 5L; // 数据中心标识位数

/** 最大值 这里的 ~(-1L << MAX_WORKER_ID) 等价于 -1L ^ (-1L << MAX_WORKER_ID) 下同
支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS); // 机器ID最大值
private static final long MAX_DATACENTER_ID = ~(-1L << DATACENTER_ID_BITS); // 数据中心ID最大值

//序列号id长度
private static final long SEQUENCE_BITS = 12L; // 毫秒内自增位
/** 序列号最大值,即生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);

// 机器ID偏左移12位
private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
//数据id需要左移位数 12+5=17位
private static final long DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
// 时间戳需要左移位数 12+5+5=22位
private static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS;



/** 上次生成ID的时间截,初始值为负数 */
private static long lastTimestamp = -1L;
// 毫秒内序列(0~4095)
private long sequence = 0L;

/** 工作机器ID(0~31) */
private final long workerId;
/** 数据中心ID(0~31) */
private final long datacenterId;

//==============================Constructors=====================================

public IdWorker() {
this.datacenterId = getDatacenterId(MAX_DATACENTER_ID);
this.workerId = getMaxWorkerId(datacenterId, MAX_WORKER_ID);
}

/**
* 构造函数
* @param workerId 工作ID (0~31)
* @param datacenterId 数据中心ID (0~31)
*/
public IdWorker(long workerId, long datacenterId) {
if (workerId > MAX_WORKER_ID || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", MAX_WORKER_ID));
}
if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", MAX_DATACENTER_ID));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}

// ==============================Methods==========================================
/**
* 获得下一个ID (该方法是线程安全的)
* @return SnowflakeId
*/
public synchronized long nextId() {
long timestamp = timeGen(); // 取得以毫秒为单位的当前时间

// 如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if (timestamp < lastTimestamp) {
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds",
lastTimestamp - timestamp));
}

// 如果是同一时间生成的,则进行毫秒内序列
// 获取当前时间戳如果等于上次时间戳(同一毫秒内),则在序列号加一;否则序列号赋值为0,从0开始。
if (lastTimestamp == timestamp) {
// 当前毫秒内,则 +1
sequence = (sequence + 1) & SEQUENCE_MASK;
// 毫秒内序列溢出
if (sequence == 0) {
// 当前毫秒内计数满了,阻塞到下一个毫秒
timestamp = tilNextMillis(lastTimestamp);
}
}
// 时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}
// 上次生成ID的时间截
lastTimestamp = timestamp;


/*
ID偏移组合生成最终的ID,并返回ID

返回结果:
((timestamp - TWEPOCH) << TIMESTAMP_LEFT_SHIFT) 表示将时间戳减去初始时间戳,再左移相应位数
(datacenterId << DATACENTER_ID_SHIFT) 表示将数据id左移相应位数
(workerId << WORKER_ID_SHIFT) 表示将工作 id左移相应位数
| 是按位或运算符,例如:x | y,只有当x,y都为0的时候结果才为0,其它情况结果都为1。
因为个部分只有相应位上的值有意义,其它位上都是0,所以将各部分的值进行 | 运算就能得到最终拼接好的id
*/
return ((timestamp - TWEPOCH) << TIMESTAMP_LEFT_SHIFT)
| (datacenterId << DATACENTER_ID_SHIFT)
| (workerId << WORKER_ID_SHIFT) | sequence;
}

/**
* 阻塞到下一个毫秒,直到获得新的时间戳(这里使用自旋锁)
* @param lastTimestamp 上次生成ID的时间截(注意这里使用 final 修饰)
* @return 当前时间戳
*/
private long tilNextMillis(final long lastTimestamp) {
long timestamp = this.timeGen(); // 取得当前时间
while (timestamp <= lastTimestamp) {
timestamp = this.timeGen();
}
return timestamp;
}

/**
* 返回以毫秒为单位的当前时间
* @return 当前时间(毫秒)
*/
private long timeGen() {
return System.currentTimeMillis();
}

/**
* 获取 maxWorkerId
*/
protected static long getMaxWorkerId(long datacenterId, long maxWorkerId) {
StringBuilder mpid = new StringBuilder();
mpid.append(datacenterId);
String name = ManagementFactory.getRuntimeMXBean().getName();
if (!name.isEmpty()) {
// GET jvmPid
mpid.append(name.split("@")[0]);
}
// MAC + PID 的 hashcode 获取16个低位
return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);
}

/**
* 数据标识id部分
*/
protected static long getDatacenterId(long maxDatacenterId) {
long id = 0L;
try {
InetAddress ip = InetAddress.getLocalHost();
NetworkInterface network = NetworkInterface.getByInetAddress(ip);
if (network == null) {
id = 1L;
} else {
byte[] mac = network.getHardwareAddress();
id = ((0x000000FF & (long) mac[mac.length - 1])
| (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6;
id = id % (maxDatacenterId + 1);
}
} catch (Exception e) {
System.out.println(" getDatacenterId: " + e.getMessage());
}
return id;
}

//==============================Test=============================================
/** 测试 */
public static void main(String[] args) {
//推特 26万个不重复的ID
IdWorker idWorker = new IdWorker(0, 0);
for (int i = 0; i < 2600; i++) {
System.out.println(idWorker.nextId());
}
}

}